SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
12.0 KB · 228 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { PriceChart } from "@/components/market/chart";5import { EventRow } from "@/components/market/event-row";6import { InstrumentHeader } from "@/components/market/instrument-header";7import { InstrumentStats } from "@/components/market/instrument-stats";8import { CoverageBadge } from "@/components/market/coverage-badge";9import { ProvenancePanel } from "@/components/market/provenance-panel";10import { RightsBadge, StatusBadge } from "@/components/ui/status-badge";11import { Empty, Kv, Section } from "@/components/ui/section";12import { api, apiOptional } from "@/lib/api";13import { ASSET_CLASS_LABEL, formatDateTime, instrumentHref } from "@/lib/format";14import type { Exchange, Filing, Instrument, InstrumentCoverage, MarketEvent, Quote } from "@/lib/types";1516export const dynamic = "force-dynamic";1718interface Detail {19  instrument: Instrument;20  company: { id: string; name: string; cik: string | null; country: string | null; sector: string | null; industry: string | null; website: string | null } | null;21  exchange: (Exchange & { status: NonNullable<Exchange["status"]> }) | null;22  quote: Quote | null;23  aliases: Array<{ alias: string; source_id: string | null }>;24  sources: string[];25  related: Instrument[];26}2728async function load(id: string) {29  return api<Detail>(`/v1/instruments/${encodeURIComponent(id)}`);30}3132export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {33  const { id } = await params;34  try {35    const d = await load(id);36    const i = d.instrument;37    const title = `${i.symbol} · ${i.name}`;38    const desc = `${i.name} (${i.symbol}) — ${ASSET_CLASS_LABEL[i.asset_class] ?? i.asset_class}${d.exchange ? ` on ${d.exchange.name}` : ""}. Canonical price, sources, confidence, history and events on Market Atlas.`;39    return { title, description: desc, alternates: { canonical: instrumentHref(i.id) }, openGraph: { title, description: desc, type: "website" } };40  } catch {41    return { title: "Instrument" };42  }43}4445export default async function InstrumentPage({ params }: { params: Promise<{ id: string }> }) {46  const { id } = await params;47  const d = await load(id);48  if (!d?.instrument) notFound();49  const i = d.instrument;50  const [events, filings, coverage] = await Promise.all([51    apiOptional<MarketEvent[]>(`/v1/events?instrument=${encodeURIComponent(i.id)}&limit=25`),52    d.company?.cik ? apiOptional<Filing[]>(`/v1/filings?cik=${encodeURIComponent(d.company.cik)}&limit=15`) : Promise.resolve(null),53    apiOptional<InstrumentCoverage>(`/v1/coverage/${encodeURIComponent(i.id)}`),54  ]);55  const isRate = i.asset_class === "TREASURY" || i.asset_class === "INTEREST_RATE" || i.asset_class === "BOND";56  const jsonLd = {57    "@context": "https://schema.org",58    "@type": "FinancialProduct",59    name: i.name,60    tickerSymbol: i.symbol,61    category: ASSET_CLASS_LABEL[i.asset_class] ?? i.asset_class,62    url: `${process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.market-atlas.co"}${instrumentHref(i.id)}`,63    ...(d.exchange ? { provider: { "@type": "Organization", name: d.exchange.name } } : {}),64  };65  return (66    <div>67      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />68      <InstrumentHeader instrument={i} quote={d.quote} exchange={d.exchange} coverage={coverage ?? undefined} />69      <div className="mx-auto max-w-[1440px] px-3 py-5 sm:px-5">70        <div className="grid grid-cols-1 [&>*]:min-w-0 gap-6 lg:grid-cols-[1fr_360px]">71          <div className="min-w-0">72            <PriceChart instrumentId={i.id} assetClass={i.asset_class} defaultRange={i.asset_class === "CRYPTO" ? "1D" : "3M"} />73            <Section title="Key statistics" hint={isRate ? "percent" : d.quote?.currency ?? undefined}>74              <InstrumentStats instrument={i} quote={d.quote} />75            </Section>76            <Section title="Why this price?" hint="source coverage matrix · every observation, its type, its role and its distance to the consensus" badge={coverage ? <CoverageBadge coverage={coverage} compact className="ml-1" /> : undefined}>77              <ProvenancePanel instrumentId={i.id} assetClass={i.asset_class} defaultOpen />78            </Section>79            <Section title="Recent events" href={`/events?instrument=${encodeURIComponent(i.id)}`} hint="canonical events touching this instrument">80              {events?.length ? (81                <ul className="rounded-md border border-rule bg-surface px-3">82                  {events.map((e) => (83                    <EventRow key={e.id} e={e} />84                  ))}85                </ul>86              ) : (87                <Empty>No canonical events for {i.symbol} yet. Events are derived from observed moves, halts, filings and source incidents.</Empty>88              )}89            </Section>90            {d.company?.cik && (91              <Section title="Regulatory filings" href={`/filings?cik=${d.company.cik}`} hint="SEC EDGAR · linked by CIK">92                {filings?.length ? (93                  <div className="overflow-x-auto rounded-md border border-rule bg-surface">94                    <table className="table-dense">95                      <thead>96                        <tr>97                          <th>Filed</th>98                          <th>Form</th>99                          <th>Filer</th>100                          <th>Document</th>101                        </tr>102                      </thead>103                      <tbody>104                        {filings.map((f) => (105                          <tr key={f.id}>106                            <td className="mono text-ink-2">{formatDateTime(f.filed_at, { tz: "America/New_York" })}</td>107                            <td className="mono font-medium">{f.form_type}</td>108                            <td>{f.company_name}</td>109                            <td>110                              <a href={f.url} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">111                                EDGAR ↗112                              </a>113                            </td>114                          </tr>115                        ))}116                      </tbody>117                    </table>118                  </div>119                ) : (120                  <Empty>No filings observed for CIK {d.company.cik} since Market Atlas started listening.</Empty>121                )}122              </Section>123            )}124          </div>125          <aside className="space-y-6">126            <div className="rounded-md border border-rule bg-surface p-4">127              <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Instrument</h3>128              <Kv129                cols={1}130                className="mt-2"131                items={[132                  ["Market Atlas id", i.id],133                  ["Class", ASSET_CLASS_LABEL[i.asset_class] ?? i.asset_class],134                  ["Security type", i.security_type?.replace(/_/g, " ").toLowerCase() ?? "—"],135                  ["Currency", i.currency ?? "—"],136                  ["Country", i.country ?? "—"],137                  ...(i.base ? ([["Base / quote", `${i.base} / ${i.quote ?? "—"}`]] as Array<[string, string]>) : []),138                  ["MIC", i.mic ?? "—"],139                ]}140              />141              {d.quote && (142                <div className="mt-3 flex flex-wrap gap-1.5">143                  <StatusBadge status={d.quote.data_status} />144                  <RightsBadge status={d.quote.rights_status} />145                </div>146              )}147            </div>148            {d.exchange && (149              <div className="rounded-md border border-rule bg-surface p-4">150                <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Venue</h3>151                <Link href={`/exchanges/${d.exchange.id}`} className="mt-1 block font-medium hover:underline">152                  {d.exchange.name}153                </Link>154                <div className="mt-1 flex items-center gap-2 text-sm text-ink-2">155                  <StatusBadge status={d.exchange.status.state} /> local {d.exchange.status.localTime} · {d.exchange.timezone}156                  {d.exchange.status.isHoliday && d.exchange.status.holidayName ? ` · ${d.exchange.status.holidayName}` : ""}157                </div>158                {d.exchange.status.nextTransition && (159                  <div className="mt-1 text-xs text-ink-3">160                    Next: {d.exchange.status.nextTransition.state.toLowerCase()} at {formatDateTime(d.exchange.status.nextTransition.at, { tz: d.exchange.timezone })}161                  </div>162                )}163              </div>164            )}165            {d.company && (166              <div className="rounded-md border border-rule bg-surface p-4">167                <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Company</h3>168                <div className="mt-1 font-medium">{d.company.name}</div>169                <Kv cols={1} className="mt-1" items={[["CIK", d.company.cik ?? "—"], ["Country", d.company.country ?? "—"], ["Sector", d.company.sector ?? "—"]]} />170                {d.company.cik && (171                  <a href={`https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=${d.company.cik}`} target="_blank" rel="noopener noreferrer" className="mt-2 inline-block text-xs text-accent hover:underline">172                    EDGAR company page ↗173                  </a>174                )}175              </div>176            )}177            <div className="rounded-md border border-rule bg-surface p-4">178              <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Observed by</h3>179              {coverage && <CoverageBadge coverage={coverage} className="mt-1.5" />}180              {d.sources.length ? (181                <ul className="mt-1 flex flex-wrap gap-1.5">182                  {d.sources.map((s) => (183                    <li key={s}>184                      <Link href={`/sources#${s}`} className="mono rounded border border-rule px-1.5 py-0.5 text-xs text-ink-2 hover:border-rule-strong">185                        {s}186                      </Link>187                    </li>188                  ))}189                </ul>190              ) : (191                <p className="mt-1 text-xs text-ink-3">No source has published a value for this instrument in the current process yet.</p>192              )}193              {d.aliases.length > 0 && (194                <>195                  <h3 className="mt-3 text-[11px] font-medium uppercase tracking-wide text-ink-3">Aliases</h3>196                  <ul className="mt-1 flex flex-wrap gap-1.5">197                    {d.aliases.slice(0, 12).map((a) => (198                      <li key={`${a.alias}-${a.source_id}`} className="mono rounded bg-surface-2 px-1.5 py-0.5 text-xs text-ink-2" title={a.source_id ? `alias used by ${a.source_id}` : "global alias"}>199                        {a.alias}200                        {a.source_id ? <span className="text-ink-3"> · {a.source_id}</span> : null}201                      </li>202                    ))}203                  </ul>204                </>205              )}206            </div>207            {d.related.length > 0 && (208              <div className="rounded-md border border-rule bg-surface p-4">209                <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Related instruments</h3>210                <ul className="mt-1 divide-y divide-rule text-sm">211                  {d.related.map((r) => (212                    <li key={r.id} className="flex items-center justify-between py-1.5">213                      <Link href={instrumentHref(r.id)} className="min-w-0 truncate hover:underline">214                        <span className="mono font-medium">{r.symbol}</span> <span className="text-xs text-ink-3">{r.name}</span>215                      </Link>216                      <span className="text-[10.5px] uppercase text-ink-3">{r.exchange_id ?? r.asset_class}</span>217                    </li>218                  ))}219                </ul>220              </div>221            )}222          </aside>223        </div>224      </div>225    </div>226  );227}228